Usted trabaja para un organismo no gubernamental que está interesado en las dinámicas socioeconómicas que determinan la desigualdad de ingreso y la erradicación de la pobreza extrema, enmarcado dentro de los objetivos del desarrollo del nuevo milenio del PNUD. Le encomiendan el desarrollo de un modelo predictivo sobre la probabilidad que un individuo presente salarios por sobre o bajo los 50.000 dólares anuales, en base a una serie de atributos sociodemográficos.
Dado que se trata de un problema de clasificación se implementará un modelo de regresión logística del tipo:
$$log \left( \frac{Pr(income=1)}{1-Prd(income=1)} \right) = \beta_0 + \sum_{i=0} {\beta }_{i} \cdot {X}_{i}$$
Como es un modelo de clasificación las métricas a usar son:
Matriz de confusión: Tabla cruzada que permite evaluar la capacidad predictiva del modelo entre las etiquetas verdaderas y las predichas.
Accuracy: Porcentaje de casos predichos correctamente sobre el total de casos.
Precision: Medición de casos positivos predichos frente a las etiquetas positivas (VP & FP).
Recall: Fracción de casos positivos predichos por el modelo.
F1: Media armónica entre Precision y Recall
ROC: Evalúa la relación entre ambos errores condicional en todo el rango del clasificador
Las variables que componen esta base se detallan a continuación:
agex: Edad del individuo.
workclass: Naturaleza de la organización que emplea al individuo.
education: Nivel educacional del individuo:Bachelors (Licenciado), Some-college (Superior incompleta), 11th (3ro medio), HS-grad (Secundaria completa), Prof-school (Escuela profesional), Assoc-acdm (Técnico superior administrativo) , Assoc-voc (Técnico superior vocacional), 9th (1ro medio), 7th-8th (7mo-8vo), 12th (4to medio), Masters (Maestría de postgrado), 1st-4th (1ro-4to básico), 10th(2do medio), Doctorate (Doctorado), 5th-6th (5to-6to), Preschool (Preescolar).
capital-gains: Ingresos generados por inversiones fuera del trabajo asalariado = Ingresos generados por inversiones fuera del trabajo asalariado.
capital-losses: Pérdidas generadas por inversiones fuera del trabajo asalariado.
fnlwgt: Ponderador muestral.
marital-status: Estado civil del individuo: Married-civ-spouse (Casado/a régimen civil), Divorced (Divorciado/a), Never-married (Soltero/a), Separated (Separado/a), Widowed (Viudo/a), Married-spouse-absent (Casado con esposo/a ausente), Married-AF-spouse (Casado/a régimen castrense).
occupation: Ocupación del individuo: Tech-support (Soporte técnico), Craft-repair (Reparaciones), Other-service (Otros servicios), Sales (Ventas), Exec-managerial (Ejecutivo administrativos), Prof-specialty (Profesores), Handlers cleaners (Aseo y ornato), Machine-op- inspct (Inspectores de maquinarias), Adm-clerical (Administrativos servicio al cliente), Farming- fishing (Pesca-ganadería), Transport-moving (Transporte), Priv-house-serv (Asesor del hogar), Protective-serv (servicios de seguridad), Armed-Forces (Fuerzas armadas).
relationship: Relación respecto a su familia Wife(Esposa), Own-child (hijo único), Husband (Esposo), Not-in-family (No pertenece a la familia), Other-relative (Familiar de otro tipo), Unmarried (Soltero).
race: Raza del encuestado White(Blanco caucásico), Asian-Pac-Islander (Isleño del Asia Pacífico), Amer-Indian-Eskimo (Pertenenciente a pueblos originarios), Other (Otro grupo), Black (Afroamericano).
sex: Sexo del encuestado.
Hours-per-week: Cantidad de horas trabajadas por semana.
native-country: País de origen. United-States, Cambodia, England, Puerto-Rico, Canada, Germany, Outlying-US(Guam USVI-etc), India, Japan, Greece, South, China, Cuba, Iran, Honduras, Philippines, Italy, Poland, Jamaica, Vietnam, Mexico, Portugal, Ireland, France, Dominican-Republic, Laos, Ecuador, Taiwan, Haiti, Columbia, Hungary, Guatemala, Nicaragua, Scotland, Thailand, Yugoslavia, El-Salvador, Trinadad&Tobago, Peru, Hong, Holand- Netherlands.
income: <=50K Si el individuo percibe ingresos inferiores a 50.000 dólares anuales, >50K si el individuo percibe ingresos superiores a 50.000 dólares anuales. Vector objetivo.
import pandas as pd
import pandas_profiling
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="ticks", palette = "pastel")
plt.style.use('seaborn')
import scipy.stats as stats
import math
import statsmodels.formula.api as smf
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, roc_curve,roc_auc_score
import missingno as msngo
import functions as funcs
from IPython.display import display, Markdown
import warnings
warnings.filterwarnings("ignore")
?Recodificación de los siguientes campos:
occupation debe recodificarse como collars siguiendo una nomenclatura similar a:
white-collar $\leftarrow$ Prof specialty, Exec-managerial, Adm-clerical, Sales, Tech-support.
blue-collar $\leftarrow$ Craft-repair, Machine-op-inspct, Transport-moving,Handlers-cleaners, Farming fishing, Protective-serv, Priv-house-serv.
others $\leftarrow$ Other-service, Armed-Forces
workclass debe recodificarse como workclass_recod siguiendo una nomenclatura similar a:
federal-gov $\leftarrow$ Federal-gov.state-level-gov $\leftarrow$ State-gov, Local-gov.self-employed $\leftarrow$ Self-emp-inc, Self-emp-not-incunemployed $\leftarrow$ Never-worked, Without-pay.education debe recodificarse como educ_recod siguiendo una nomenclatura similar a:
preschool $\leftarrow$ Preschoolelementary-school $\leftarrow$ 1st-4th, 5th-6thhigh-school $\leftarrow$ 7th-8th, 9th, 10th,11th, 12th, HS-gradcollege $\leftarrow$ Assoc-voc, Assoc-acdm, Some-collegeuniversity $\leftarrow$ Bachelors, Masters, Prof-school, Doctoratemarital-status debe recodificarse como civstatus siguiendo una nomenclatura similar a:
married $\leftarrow$ Married-civ-spouse, Married-spouse-absent, Married-AFspousedivorced $\leftarrow$ Divorcedseparated $\leftarrow$ Separatedwidowed $\leftarrow$ Widowed.native-country debe recodificarse como region donde cada país debe asignarse a uno de los 5 continentes.income debe recodificarse de forma binariadata = pd.read_csv('income-db.csv')
data.head()
df = data.replace(['?','nan'],np.nan)
df.head()
funcs.missing_values_table(df)
collar = {'Prof-specialty':'white_collar', 'Exec-managerial':'white_collar'
,'Adm-clerical':'white_collar', 'Sales':'white_collar', 'Tech-support':'white_collar'
,'Craft-repair':'blue_collar', 'Machine-op-inspct':'blue_collar'
,'Transport-moving':'blue_collar','Handlers-cleaners':'blue_collar'
,'Farming-fishing':'blue_collar', 'Protective-serv':'blue_collar'
,'Priv-house-serv':'blue_collar'
,'Other-service':'others','Armed-Forces':'others'}
df['occu_recod'] = df['occupation'].map(collar)
workclass $\rightarrow$ workclass_recodworkclass_recod = {'Federal-gov':'federal_gov','State-gov':'state_level_gov','Local-gov':'state_level_gov'
,'Self-emp-inc':'self_employed', 'Self-emp-not-inc':'self_employed'
,'Never-worked':'unemployed', 'Without-pay':'unemployed','Private':'private'}
df['workclass_recod'] = df['workclass'].map(workclass_recod)
education $\rightarrow$ educ_recodeduc_recod = {'Preschool':'preschool','1st-4th':'elementary_school', '5th-6th':'elementary_school'
,'7th-8th':'high_school','9th':'high_school','10th':'high_school','11th':'high_school'
,'12th':'high_school','HS-grad':'high_school'
,'Assoc-voc':'college','Assoc-acdm':'college','Some-college':'college'
,'Bachelors':'university', 'Masters':'university', 'Prof-school':'university','Doctorate':'university'}
df['educ_recod'] = df['education'].map(educ_recod)
marital-status $\rightarrow$ civstatuscivstatus = {'Married-civ-spouse':'married','Married-spouse-absent':'married'
,'Married-AFspouse':'married' ,'Divorced':'divorced','Separated':'separated'
,'Widowed':'widowed','Never-married':'never_married'}
df['civstatus'] = df['marital-status'].map(civstatus)
native-country $\rightarrow$ regioncontinent = {'United-States':'north_america', 'Canada':'north_america'
,'Peru':'latin_america_caribe', 'Guatemala':'latin_america_caribe'
,'Mexico':'latin_america_caribe','Dominican-Republic':'latin_america_caribe'
,'Haiti':'latin_america_caribe', 'El-Salvador':'latin_america_caribe', 'Puerto-Rico':'latin_america_caribe'
,'Nicaragua':'latin_america_caribe', 'Honduras':'latin_america_caribe', 'Jamaica':'latin_america_caribe'
,'Ecuador':'latin_america_caribe', 'Cuba':'latin_america_caribe','Columbia':'latin_america_caribe'
,'Trinadad&Tobago':'latin_america_caribe'
,'Ireland':'europe', 'Germany':'europe','Poland':'europe','England':'europe','Portugal':'europe'
,'Italy':'europe','Scotland':'europe', 'Yugoslavia':'europe', 'Hungary':'europe', 'Greece':'europe','France':'europe'
,'Holand-Netherlands':'europe'
,'Philippines':'asia_oceania','Thailand':'asia_oceania','India':'asia_oceania', 'Vietnam':'asia_oceania'
,'Japan':'asia_oceania','Cambodia':'asia_oceania'
,'South':'asia_oceania','Laos':'asia_oceania','Taiwan':'asia_oceania','China':'asia_oceania'
,'Iran':'asia_oceania','Hong':'asia_oceania','Outlying-US(Guam-USVI-etc)':'asia_oceania'}
df['region'] = df['native-country'].map(continent)
incomedf['income'].value_counts('%')
df['income_binarize'] = np.where(df['income'] == '>50K', 1, 0).astype('int32')
df_analytics = df.drop(columns=['occupation', 'workclass', 'education', 'marital-status'])
prof = pandas_profiling.ProfileReport(df_analytics)
prof.to_file(output_file='output.html')
prof
El 0,8% de la data está con valores perdidos concentrándose en las variables occu_recod, native-country y workclass_recod
Entendiendo que capital-gains y capital-loss tienen una alta tasa de valores ceros, estos serán eliminados
mandatory educationComo no hay una definición clara del campo fnlwgt y su cálculo sobre la muestra se eliminará
el 50% de la variable age se concentra entre los 17 y 37 años por lo que es posible realizar una normalización de la misma, dado que es asintótica a la izquierda
df_analytics2 = df_analytics.drop_duplicates()
df_analytics3 = df_analytics2.drop(columns=['capital-gain', 'capital-loss','fnlwgt'])
df_analytics4 = df_analytics3.dropna()
df_analytics4.rename(columns=lambda x: x.replace('occu_recod', 'collars'), inplace=True)
df_analytics4.info()
categorical = ['relationship', 'race', 'gender', 'region', 'collars'
, 'workclass_recod', 'educ_recod','civstatus','income']
funcs.countplot_data(df_analytics4[categorical])
relationship
41% de los encuestados son esposos dentro en la relación
37% no conforma una familia o no está casado
race
gender
region
collars
workclass_recod
educ_recod
44% tiene sus estudios secundarios completos
30% tiene college
25% tiene estudios universitarios o de posgrado
civstatus
32% nunca se ha caso, mientras 48% sí lo ha hecho
14% está divorciado
Dada la poca proporción de la categoría separated es conviente agruparla con divorced
income
categorical_income = ['relationship', 'race', 'gender', 'region', 'collars'
, 'workclass_recod', 'educ_recod','civstatus']
fig, ax = plt.subplots(2,4, figsize=(25,12))
for variable, subplot in zip(categorical_income, ax.flatten()):
y = df_analytics4.groupby(variable)["income"].value_counts(normalize=True).unstack()
y.plot(kind="bar", stacked="True", ax = subplot)
sns.set(style="ticks", palette = "pastel")
relationship
race
gender
region
collars
workclass_recod
educ_recod
civstatus
funcs.boxplot_data(df_analytics4,categorical,'age')
funcs.boxplot_data(df_analytics4,categorical,'hours-per-week')
relationship, region, race)income se observa que para los que ganan a lo más 50 mil anual sus horas trabajadas son más dispersas, lo que podría suponer hay que trabajar más horas para obtener un ingreso mayor, o bien, el trabajo en el que se está es part time, proyectos, etcage y su transformación logarítmica¶df_analytics4['age_norm'] = np.log(df_analytics4['age'])
numerical_norm= ['age', 'age_norm']
fig, ax = plt.subplots(1, 2, figsize=(8, 4))
for var, subplot in zip(numerical_norm, ax.flatten()):
sns.distplot(df_analytics4[var], ax=subplot)
fig.suptitle('Comparación entre la variable age y su normalización')
age_rank = [-math.inf,30,44,math.inf]
categorias = ['entry','mature','senior']
df_analytics4['categorical_age'] = pd.cut(x = df_analytics4['age'], bins = age_rank, labels = categorias)
df_analytics4['categorical_age'].value_counts('%')
df_analytics4.head()
hours-per-week y binarización¶sns.kdeplot(df_analytics4['hours-per-week']);
df_analytics4['hpw'] = np.where(df_analytics4['hours-per-week'] <= 40, 1, 0)
df_analytics4['hpw'].value_counts('%')
educ_recod
* mandatory_school: preeschool, elementary-school, highschool
* college
* university
educ_recod_2 = {'preschool':'mandatory_school','elementary_school':'mandatory_school', 'high_school':'mandatory_school'
,'college':'college','university':'university'}
df_analytics4['educ_recod_final'] = df['educ_recod'].map(educ_recod_2)
df_analytics4.head(5)
civstatus
* married-widowed: widowed, married
* separated: separated,divorced
* single: never-married
civstatus_recod = {'widowed':'married_widowed','separated':'separated', 'divorced':'separated'
,'never_married':'single','married':'married_widowed'}
df_analytics4['civstatus_recod'] = df_analytics4['civstatus'].map(civstatus_recod)
df_analytics4.head()
race
* Black: Black
* White: White
* Others: Other, Amer-Indian-Eskimo, Asian-Pac-Islander
race_recod = {'Black':'black','White':'white','Amer-Indian-Eskimo':'others'
,'Asian-Pac-Islander':'others','Other':'others'}
df_analytics4['race_recod'] = df_analytics4['race'].map(race_recod)
df_analytics4.head()
variable_recod = ['race','race_recod','educ_recod','educ_recod_final'
,'civstatus','civstatus_recod']
funcs.countplot_data(df_analytics4[variable_recod])
df_analytics4.head()
df_analytics4['sex'] = np.where(df_analytics4['gender'] == 'Male', 1, 0)
variables_dummy = ['relationship','race_recod','collars','educ_recod_final','civstatus_recod'
,'workclass_recod','categorical_age','region']
data_model_preproc = funcs.createDummies(df_analytics4,variables_dummy)
data_model_preproc.head()
data_model_preproc.info()
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Husband','relationship_husband'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Not-in-family','relationship_not_in_family'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Other-relative','relationship_other_relative'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Own-child','relationship_own_child'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Unmarried','relationship_unmarried'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Wife','relationship_wife'), inplace=True)
data_model_preproc.info()
relationship no será considerada en el modelo, dado que hay categorías que pueden ser medidas con civstatusregresores = list(data_model_preproc.columns)
exclusiones = ['age','categorical_age_mature','educational-num','race','gender','hours-per-week','native-country'
,'income','educ_recod','civstatus','region','income_binarize','age_norm','race_recod_white'
,'region_north_america','relationship_husband','civstatus_recod_married_widowed'
,'collars_white_collar','workclass_recod_private','educ_recod_final_mandatory_school'
,'relationship_not_in_family','relationship_other_relative','relationship_own_child'
,'relationship_unmarried','relationship_wife']
regresores = [e for e in regresores if e not in list(exclusiones)]
inputs = ' + '.join(regresores)
modelo = 'income_binarize ~ ' + inputs
logit1 = smf.logit(modelo,data_model_preproc).fit()
logit1.summary2()
funcs.significant_pvalues(logit1)
exclusiones_2 = list(funcs.not_significant_pvalues(logit1).index)
exclusiones_2
regresores2 = [e for e in regresores if e not in list(exclusiones_2)]
regresores2
inputs2 = ' + '.join(regresores2)
modelo2 = 'income_binarize ~ ' + inputs2
logit2 = smf.logit(modelo2,data_model_preproc).fit()
logit2.summary2()
prob_params = round((100 * logit2.params[1:] / 4).sort_values(),3)
prob_params
for index, value in enumerate(prob_params):
print('* Si el individuo forma parte del grupo {},{} la probabilidad de percibir más de 50.000 USD variaría en {}% {}'
.format(prob_params.keys()[index],'\n',round(value,1),'\n'))
Para efectos del análisis se usarán como atributos los regresores que en el Hito 3 fueron estadísticamente significativos
Se trabajará con la librería scikit-learn y en particular LogisticRegression
Se calcularán 3 modelos:
data_to_predict = ['income_binarize','civstatus_recod_single','civstatus_recod_separated'
,'collars_others','categorical_age_entry','region_latin_america_caribe','collars_blue_collar','hpw','region_asia_oceania'
,'workclass_recod_self_employed','race_recod_black'
,'categorical_age_senior','workclass_recod_federal_gov','educ_recod_final_college'
,'sex','educ_recod_final_university']
data_model = data_model_preproc[data_to_predict]
data_model.head()
attributes = list(data_to_predict)
attributes.remove('income_binarize')
target = ['income_binarize']
X_train, X_test, Y_train, Y_test = train_test_split(data_model.loc[:, attributes],data_model.loc[:,target], test_size=0.33, random_state=11238)
X_train_std = StandardScaler().fit_transform(X_train)
X_test_std = StandardScaler().fit_transform(X_test)
model_1 = LogisticRegression().fit(X_train_std, Y_train)
yhat = model_1.predict(X_test_std)
yhat_prob_1 = model_1.predict_proba(X_test_std)[:, 1]
metric_table_1 = pd.DataFrame(classification_report(Y_test,yhat,output_dict=True))
display(metric_table_1)
print('\nAUC:', round(roc_auc_score(Y_test, yhat_prob_1),5), '\n')
model_2 = LogisticRegression(class_weight='balanced').fit(X_train_std, Y_train)
yhat_2 = model_2.predict(X_test_std)
yhat_prob_2 = model_2.predict_proba(X_test_std)[:, 1]
metric_table_2 = pd.DataFrame(classification_report(Y_test,yhat_2,output_dict=True))
display(metric_table_2)
print('\nAUC:', round(roc_auc_score(Y_test, yhat_prob_2),5), '\n')
model_3 = LogisticRegression(fit_intercept=False, class_weight='balanced').fit(X_train_std, Y_train)
yhat_3 = model_3.predict(X_test_std)
yhat_prob_3 = model_3.predict_proba(X_test_std)[:, 1]
metric_table_3 = pd.DataFrame(classification_report(Y_test,yhat_3,output_dict=True))
display(metric_table_3)
print('\nAUC:', round(roc_auc_score(Y_test, yhat_prob_3),5), '\n')
Observando los resultados de las métricas de recall (0,68) y f1-score (0,64) el modelo_2 es el que tiene mejor desempeño entre los 3, además su accuracy (0,78) es bastante bueno y su precision aceptable (0,54)
Además el AUC es levemente superior en el modelo_2 por sobre los otros 2
Los 3 modelos predicen mucho mejor que un clasificador random la probabilidad de que un individuo perciba más de 50 mil dólares al año
model_2¶funcs.plot_roc_curve(Y_test,yhat_prob_2)